import { OnInit, Component, ChangeDetectionStrategy, Input, ViewEncapsulation,
  ViewChild, ElementRef, Inject, ChangeDetectorRef, OnDestroy, forwardRef, Output, EventEmitter } from '@angular/core';
import { fromEvent, Observable, Subscription } from 'rxjs';
import { tap, pluck, map, distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { DOCUMENT } from '@angular/common';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-music-slider',
  templateUrl: './music-slider.component.html',
  styleUrls: ['./music-slider.component.less'],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => MusicSliderComponent),
    multi: true
  }]
})
export class MusicSliderComponent implements OnInit, OnDestroy, ControlValueAccessor {

  @Input('vertical')
  isVertical = false;

  @Input()
  bufferOffset: number;

  @Output('onSliderLengthChange')
  sliderLengthChange = new EventEmitter<number>();

  @ViewChild('slider', { static: true })
  sliderElementRef: ElementRef;

  sliderMin = 0;
  sliderMax = 100;

  isDragging = false;
  sliderValue: number;
  offset: number;

  private sliderDom: HTMLDivElement;

  private mouseStartObservable: Observable<number>;
  private mouseMoveObservable: Observable<number>;
  private mouseEndObservable: Observable<Event>;

  private mouseStartSubscription: Subscription;
  private mouseMoveSubscription: Subscription;
  private mouseEndSubscription: Subscription;

  private onValueChange = (value: number) => {};
  private onTouched = () => {};

  constructor(
    @Inject(DOCUMENT) private doc: Document,
    private changeDetectorRef: ChangeDetectorRef
  ) { }

  ngOnInit(): void {
    // 获取 slider 组件的 DOM 元素
    this.sliderDom = this.sliderElementRef.nativeElement;
    this.createDragingObservales();
    this.subscribeDrag(['start']);
  }

  ngOnDestroy(): void {
    // 取消拖拽事件订阅
    this.unsubscribeDrag();
  }

  /** 创建鼠标拖拽事件 */
  private createDragingObservales(): void {
    const mouseEvent = {
      start: 'mousedown',
      move: 'mousemove',
      end: 'mouseup'
    };
    // 绑定鼠标按下事件
    this.mouseStartObservable = fromEvent(this.sliderDom, mouseEvent.start)
      .pipe(
        tap(this.sliderEvent),
        // 提取鼠标的位置：pageX, pageY
        pluck(this.isVertical ? 'pageY' : 'pageX'),
        map((position: number) => this.findClosestPosition(position))
      );
    // 绑定鼠标放开事件
    this.mouseEndObservable = fromEvent(this.doc, mouseEvent.end);
    // 绑定鼠标移动事件
    this.mouseMoveObservable = fromEvent(this.doc, mouseEvent.move)
      .pipe(
        tap(this.sliderEvent),
        // 提取鼠标的位置：pageX, pageY
        pluck(this.isVertical ? 'pageY' : 'pageX'),
        distinctUntilChanged(),
        map((position: number) => this.findClosestPosition(position)),
        takeUntil(this.mouseEndObservable)
      );
  }

  private sliderEvent(e: Event): void {
    // 停止传播
    e.stopPropagation();
    // 消除默认事件
    e.preventDefault();
  }

  /** 订阅拖拽事件 */
  private subscribeDrag(events: string[] = ['start', 'move', 'end']): void {
    if (this.inArray(events, 'start') && this.mouseStartObservable && !this.mouseStartSubscription) {
      this.mouseStartSubscription = this.mouseStartObservable.subscribe(this.onDragStart.bind(this));
    }
    if (this.inArray(events, 'move') && this.mouseMoveObservable && !this.mouseMoveSubscription) {
      this.mouseMoveSubscription = this.mouseMoveObservable.subscribe(this.onDragMove.bind(this));
    }
    if (this.inArray(events, 'end') && this.mouseEndObservable && !this.mouseEndSubscription) {
      this.mouseEndSubscription = this.mouseEndObservable.subscribe(this.onDragEnd.bind(this));
    }
  }

  /** 取消拖拽事件订阅 */
  private unsubscribeDrag(events: string[] = ['start', 'move', 'end']): void {
    if (this.inArray(events, 'start') && this.mouseStartSubscription) {
      this.mouseStartSubscription.unsubscribe();
      this.mouseStartSubscription = null;
    }
    if (this.inArray(events, 'move') && this.mouseMoveSubscription) {
      this.mouseMoveSubscription.unsubscribe();
      this.mouseMoveSubscription = null;
    }
    if (this.inArray(events, 'end') && this.mouseEndSubscription) {
      this.mouseEndSubscription.unsubscribe();
      this.mouseEndSubscription = null;
    }
  }

  private inArray(array: any[], target: any): boolean {
    return array.indexOf(target) !== -1;
  }

  private onDragStart(value: number): void {
    this.setSliderValue(value);
    this.toggleDragMoving(true);
  }

  private onDragMove(value: number): void {
    if (this.isDragging) {
      this.setSliderValue(value);
      this.changeDetectorRef.markForCheck();
    }
  }

  private onDragEnd(): void {
    this.sliderLengthChange.emit(this.sliderValue);
    // 取消拖拽事移动
    this.toggleDragMoving(false);
  }

  private toggleDragMoving(movable: boolean): void {
    this.isDragging = movable;
    if (movable) {
     // 订阅拖拽事件
     this.subscribeDrag(['move', 'end']);
     return;
    }
    // 取消拖拽事件订阅
    this.unsubscribeDrag(['move', 'end']);
  }
 
  private setSliderValue(value: number): void {
    if (this.sliderValue !== value) {
      this.sliderValue = value;
      this.updateTrackAndHandle();
    }
    this.onValueChange(this.sliderValue);
  }
  
  /** 更新滑块的滑槽和滑点位置 */
  private updateTrackAndHandle(): void {
    this.offset = this.getValueToOffset(this.sliderValue);
    this.changeDetectorRef.markForCheck();
  }

  /** 获取偏移量 */
  private getValueToOffset(value: number): number {
    return this.getPercent(value, this.sliderMin, this.sliderMax);
  }

  /** 获取百分比 */
  private getPercent(value: number, min: number, max: number): number {
    return ((value - min) / (max - min)) * 100;
  }

  /** 根据鼠标位置获取最近的坐标 */
  private findClosestPosition(position: number): number {
    // 滑块总长度
    const sliderLength = this.getSliderLength();
    // 获取端点位置（左，上）
    const sliderStart = this.getSliderStartPosition();
    // 滑块当前位置 / 滑块总长
    const ratio = this.limitNumberInRange((position - sliderStart) / sliderLength, 0, 1);
    const ratioTrue = this.isVertical ? 1 - ratio : ratio;

    return ratioTrue * (this.sliderMax - this.sliderMin) + this.sliderMin;
  }

  /**
   * 将 value 值限制在 min 和 max 之间
   *
   * @param value 值
   * @param min  最小值
   * @param max  最大值
   */
  private limitNumberInRange(value: number, min: number, max: number): number {
    return Math.min(Math.max(value, min), max);
  }

  /** 获取滑块总长 */
  private getSliderLength(): number {
    return this.isVertical ? this.sliderDom.clientHeight : this.sliderDom.clientWidth;
  }
  
  /** 获取滑块的端点位置 */
  private getSliderStartPosition(): number {
    const offset = this.getElementOffset(this.sliderDom);
    return this.isVertical ? offset.top : offset.left;
  }

  /**
   * 获取指定元素的偏移量
   *
   * @param el DOM 元素
   */
  private getElementOffset(el: HTMLElement): { top: number, left: number } {
    if (!el.getClientRects().length) {
      return { top: 0, left: 0 };
    }
    // 获取元素位置信息
    const rect = el.getBoundingClientRect();
    // 获取元素所在的 window 对象
    const win = el.ownerDocument.defaultView;
    return {
      top: rect.top + win.pageYOffset,
      left: rect.left + win.pageXOffset
    };
  }

  private formatValue(value: number): number {
    if (this.assertValueValid(value)) {
      return this.sliderMin;
    }
    return this.limitNumberInRange(value, this.sliderMin, this.sliderMax);
  }

  /** 判断传入的值是否是NaN */
  private assertValueValid(value: number): boolean {
    return isNaN(typeof value !== 'number' ? parseFloat(value) : value);
  }

  writeValue(obj: any): void {
    if (obj) {
      if (this.isDragging) {
        return;
      }
      const newValue = this.formatValue(obj);
      this.setSliderValue(newValue);
    }
  }

  registerOnChange(fn: any): void {
    this.onValueChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

}
